﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace Luminal
{
    public class KernelCollection : ReadOnlyCollection<Kernel>
    {
        public KernelCollection(IEnumerable<Kernel> kernels)
            : base(kernels.ToList())
        {

        }

        public Kernel this[string functionName]
        {
            get
            {
                foreach (Kernel kernel in this)
                    if (kernel.FunctionName == functionName)
                        return kernel;

                throw new KeyNotFoundException();
            }
        }
    }

    public sealed class Kernel : LuminalObject
    {
        #region Interop
        [DllImport("OpenCL.dll")]
        static extern ErrorCode clReleaseKernel(IntPtr kernel);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clSetKernelArg(IntPtr kernel,
            uint arg_index,
            SizeT arg_size,
            IntPtr arg_value);

        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetKernelInfo(IntPtr kernel,
            cl_kernel_info param_name,
            SizeT param_value_size,
            IntPtr param_value,
            out SizeT param_value_size_ret);
        #endregion

        #region Properties
        public Context Context { get; private set; }
        public Program Program { get; private set; }
        public string FunctionName { get; private set; }
        public int ArgumentCount { get; private set; }

        private T GetInfo<T>(cl_kernel_info info)
        {
            Type type = typeof(T);
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            SizeT dataSize;
            Util.ThrowError(
                "Could not get program info.",
                clGetKernelInfo(InternalPointer, info, 0, IntPtr.Zero, out dataSize));

            if (type.IsValueType)
            {
                byte[] outputBytes = new byte[dataSize];
                Util.Pin(outputBytes, ptr =>
                {
                    Util.ThrowError(
                        "Could not get kernel info.",
                        clGetKernelInfo(InternalPointer, info, dataSize, ptr, out dataSize));
                });

                IntPtr outputPtr = Marshal.AllocHGlobal(outputBytes.Length);
                Marshal.Copy(outputBytes, 0, outputPtr, outputBytes.Length);

                T output = (T)Marshal.PtrToStructure(outputPtr, type);
                Marshal.FreeHGlobal(outputPtr);

                return output;
            }
            else if (type == typeof(string))
            {
                IntPtr outputPtr = Marshal.AllocHGlobal((int)dataSize);
                Util.ThrowError(
                    "Could not get kernel info.",
                    clGetKernelInfo(InternalPointer, info, dataSize, outputPtr, out dataSize));

                string output = Marshal.PtrToStringAnsi(outputPtr);
                Marshal.FreeHGlobal(outputPtr);

                return (T)(object)output;
            }
            else
            {
                throw new Exception("Type not supported.");
            }
        }
        #endregion

        #region Constructor
        private Kernel(IntPtr ptr)
        {
            Initialize(ptr);

            try
            {
                Context = Context.FromPointer(GetInfo<IntPtr>(cl_kernel_info.CL_KERNEL_CONTEXT));
                Program = Program.FromPointer(GetInfo<IntPtr>(cl_kernel_info.CL_KERNEL_PROGRAM));
                FunctionName = GetInfo<string>(cl_kernel_info.CL_KERNEL_FUNCTION_NAME);
                ArgumentCount = (int)GetInfo<uint>(cl_kernel_info.CL_KERNEL_NUM_ARGS);
            }
            catch (Exception)
            {
                Dispose();
                throw;
            }
        }
        #endregion

        #region Methods
        public void SetArgument<T>(int index, T value)
        {
            Type type = typeof(T);
            if (type.IsEnum)
                type = Enum.GetUnderlyingType(type);

            if (type.IsValueType)
            {
                int typeSize = Marshal.SizeOf(typeof(T));

                IntPtr ptr = Marshal.AllocHGlobal(typeSize);
                Marshal.StructureToPtr(value, ptr, true);
                Util.ThrowError("Could not set argument.", clSetKernelArg(InternalPointer, (uint)index, typeSize, ptr));
                Marshal.FreeHGlobal(ptr);
            }
            else if (type == typeof(MemoryObject) || type == typeof(Sampler))
            {
                LuminalObject obj = value as LuminalObject;
                SetArgument<IntPtr>(index, obj.InternalPointer);
            }
            else
            {
                throw new ArgumentException("The argument type is not supported by this function.", "T");
            }
        }
        #endregion

        protected override void Dispose(bool disposing)
        {
            if (disposing)
                Util.ThrowError("Could not release kernel object.", clReleaseKernel(InternalPointer));

            base.Dispose(disposing);
        }

        public static Kernel FromPointer(IntPtr ptr)
        {
            return ObjectTracker.ContainsObject(ptr) ? ObjectTracker.GetObject<Kernel>(ptr) : new Kernel(ptr);
        }
    }
}
